﻿using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Dynamic;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using EpiNEXT.Core.Fields;
using EpiNEXT.Core.Validation;
using EpiNEXT.Core.Validation.Actions;

namespace EpiNEXT.Core
{
    /// <summary>
    /// An object designed to represent a single record, and for which different properties must be exposed
    /// depending on the data source that has been read.
    /// </summary>
    public class Record : ValidatableDynamicObservableObject
    {
        #region Members
        /// <summary>
        /// The foreign key for this record
        /// </summary>
        private Guid _foreignKey = new Guid("00000000000000000000000000000000");

        /// <summary>
        /// Whether or not this record has unsaved changes
        /// </summary>
        private bool _hasUnsavedChanges = false;

        /// <summary>
        /// Whether or not this record exists in the database
        /// </summary>
        private bool _isInDatabase = false;
        #endregion // Members

        #region Constructors
        /// <summary>
        /// Constructor
        /// </summary>
        public Record()
        {
            // Set the GUID value and other properties that are always going to be common across data sets
            this.Id = System.Guid.NewGuid();
            this.IdString = Id.ToString();
            this.RecStatus = 1;
            this.FirstSaveTime = DateTime.Now;
            this.FirstSaveLogOn = System.Security.Principal.WindowsIdentity.GetCurrent().Name.ToString();
            this.LastSaveTime = DateTime.Now;
            this.LastSaveLogOn = System.Security.Principal.WindowsIdentity.GetCurrent().Name.ToString();
            this.SuppressErrorNotification = true;
            // Note we can't set FKEY here because we don't have a parent form reference, so this has to be attached
            // later when we're dealing with child records.
        }
        #endregion // Constructors

        #region Properties

        /// <summary>
        /// Gets whether or not this record has unsaved changes
        /// </summary>
        public bool HasUnsavedChanges
        {
            get
            {
                return this._hasUnsavedChanges;
            }
            internal set
            {
                if (value != HasUnsavedChanges)
                {
                    this._hasUnsavedChanges = value;
                    RaisePropertyChanged("HasUnsavedChanges");
                    if (HasUnsavedChanges == false)
                    {
                        DataChanges.Clear();
                    }
                }
            }
        }

        /// <summary>
        /// Gets whether or not this record exists in the database
        /// </summary>
        /// <remarks>
        /// This property doesn't specify whether the record in memory matches the record on disk - only 
        /// that the GUIDs match. The purpose of this is to figure out which records in memory should be
        /// used in update queries and which ones should be used in insertion queries when a save (or
        /// similar operation) is initiated.
        /// </remarks>
        public bool IsInDatabase
        {
            get
            {
                return this._isInDatabase;
            }
            internal set
            {
                if (value != IsInDatabase)
                {
                    this._isInDatabase = value;
                    RaisePropertyChanged("IsInDatabase");
                }
            }
        }

        /// <summary>
        /// The form this record belongs to
        /// </summary>
        public DataEntryInstrument Form { get; set; }

        /// <summary>
        /// The ID for this record
        /// </summary>
        public Guid Id { get; set; }

        /// <summary>
        /// The time the record was first saved
        /// </summary>
        public DateTime FirstSaveTime { get; set; }

        /// <summary>
        /// The time the record was last saved
        /// </summary>
        public DateTime LastSaveTime { get; set; }

        /// <summary>
        /// The user ID of the person who created the record
        /// </summary>
        public string FirstSaveLogOn { get; set; }

        /// <summary>
        /// The user ID of the person who last saved the record
        /// </summary>
        public string LastSaveLogOn { get; set; }

        /// <summary>
        /// The record status (currently 1= active, 0= soft-deleted, to mimic Epi Info 7)
        /// </summary>
        public short RecStatus { get; set; }

        /// <summary>
        /// String representation of the foreign key
        /// </summary>
        public string ForeignKeyString { get; private set; }

        /// <summary>
        /// String representation of the record ID
        /// </summary>
        public string IdString { get; private set; }

        /// <summary>
        /// The foreign key value; this should match an ID value of a record in a parent form
        /// </summary>
        public Guid ForeignKey
        {
            get
            {
                return this._foreignKey;
            }
            set
            {
                this._foreignKey = value;
                ForeignKeyString = ForeignKey.ToString();
                RaisePropertyChanged("ForeignKey");
            }
        }

        public string this[string propertyName]
        {
            get
            {
                string result = string.Empty;

                if (SuppressErrorNotification == false)
                {
                    propertyName = propertyName ?? string.Empty;

                    if (this.Form != null)
                    {
                        KeyValuePair<string, object> kvp = new KeyValuePair<string, object>();
                        // Loop through all properties on this object
                        foreach (KeyValuePair<string, object> iKvp in Data)
                        {
                            if (iKvp.Key == propertyName)
                            {
                                kvp = iKvp;
                                break;
                            }
                        }


                        // First, let's find the associated field for this property so we can check the metadata
                        string associatedFieldName = kvp.Key;
                        IField associatedField = null;
                        foreach (IField field in this.Form.Fields)
                        {
                            if (field.Name == associatedFieldName)
                            {
                                associatedField = field;
                                break;
                            }
                        }

                        // Should never be null, but check anyway
                        if (associatedField != null)
                        {
                            if (associatedField is DataField)
                            {
                                DataField dataField = associatedField as DataField;

                                if (dataField.IsRequired && (kvp.Value == null || String.IsNullOrEmpty(kvp.Value.ToString())))
                                {
                                    result = kvp.Key + " is required.";
                                    return result;
                                }
                            }

                            // If it's a number field
                            if (associatedField is NumberField && kvp.Value != null && !String.IsNullOrEmpty(kvp.Value.ToString()))
                            {
                                NumberField numberField = (associatedField as NumberField);
                                // Test #1 - make sure the user's input is valid
                                decimal testValue;
                                bool success = decimal.TryParse(kvp.Value.ToString(), out testValue);
                                if (!success)
                                {
                                    result = kvp.Key + " is invalid.";
                                }
                                else
                                {
                                    // Test #2 - check the upper and lower boundaries
                                    if (numberField.LowerBound.HasValue && testValue < numberField.LowerBound.Value)
                                    {
                                        result = kvp.Key + " is less than the lower boundary of " + numberField.LowerBound.Value.ToString() + ".";
                                    }
                                    else if (numberField.UpperBound.HasValue && testValue > numberField.UpperBound.Value)
                                    {
                                        result = kvp.Key + " is greater than the upper boundary of " + numberField.UpperBound.Value.ToString() + ".";
                                    }
                                }
                            }
                            // If it's a date field
                            else if (associatedField is DatePickerField && kvp.Value != null && !String.IsNullOrEmpty(kvp.Value.ToString().Trim()))
                            {
                                DatePickerField datePickerField = (associatedField as DatePickerField);
                                DateTime testValue = Convert.ToDateTime(kvp.Value);

                                // Test #2 - check the upper and lower boundaries
                                if (datePickerField.LowerBound.HasValue && testValue < (new DateTime(datePickerField.LowerBound.Value)))
                                {
                                    result = kvp.Key + " is less than the lower boundary of " + (new DateTime(datePickerField.LowerBound.Value)).ToString() + ".";
                                }

                                else if (datePickerField.UpperBound.HasValue && testValue > (new DateTime(datePickerField.UpperBound.Value)))
                                {
                                    result = kvp.Key + " is greater than the upper boundary of " + (new DateTime(datePickerField.LowerBound.Value)).ToString() + ".";
                                }
                            }
                            // If it's a date box field
                            else if (associatedField is DateBoxField && kvp.Value != null && !String.IsNullOrEmpty(kvp.Value.ToString().Trim()))
                            {
                                DateBoxField dateBoxField = (associatedField as DateBoxField);
                                
                                DateTime testValue = DateTime.Now;
                                bool success = DateTime.TryParse(kvp.Value.ToString(), out testValue);
                                if (!success)
                                {
                                    string actualSeparators = dateBoxField.InputMask.Replace("0", String.Empty);
                                    if (actualSeparators.Length > 0)
                                    {
                                        string unmaskedValue = kvp.Value.ToString().Replace("_", String.Empty).Replace(actualSeparators[0].ToString(), String.Empty).Replace(actualSeparators[actualSeparators.Length - 1].ToString(), String.Empty).Trim();

                                        if (!String.IsNullOrEmpty(unmaskedValue))
                                        {
                                            result = kvp.Value + " is an invalid date/time value.";
                                        }
                                    }
                                }
                                else
                                {
                                    // Test #2 - check the upper and lower boundaries
                                    if (dateBoxField.LowerBound.HasValue && testValue < (new DateTime(dateBoxField.LowerBound.Value)))
                                    {
                                        result = kvp.Key + " is less than the lower boundary of " + (new DateTime(dateBoxField.LowerBound.Value)).ToString() + ".";
                                    }

                                    else if (dateBoxField.UpperBound.HasValue && testValue > (new DateTime(dateBoxField.UpperBound.Value)))
                                    {
                                        result = kvp.Key + " is greater than the upper boundary of " + (new DateTime(dateBoxField.LowerBound.Value)).ToString() + ".";
                                    }
                                }
                            }
                            else if (associatedField is TextBoxField && kvp.Value != null && !String.IsNullOrEmpty((associatedField as TextBoxField).RegEx))
                            {
                                TextBoxField textBoxField = (associatedField as TextBoxField);
                                string expression = textBoxField.RegEx;
                                bool success = System.Text.RegularExpressions.Regex.IsMatch(kvp.Value.ToString(), expression, System.Text.RegularExpressions.RegexOptions.IgnoreCase);
                                if (!success)
                                {
                                    if (String.IsNullOrEmpty(textBoxField.RegExMessage.Trim()))
                                    {
                                        result = "Invalid value.";
                                    }
                                    else
                                    {
                                        result = textBoxField.RegExMessage;
                                    }

                                    foreach (FieldTextPropertyTranslation regExMessage in textBoxField.RegExMessageTranslations)
                                    {
                                        if (regExMessage.Culture.Equals(System.Threading.Thread.CurrentThread.CurrentUICulture.ToString()) && !String.IsNullOrEmpty(regExMessage.Text.Trim()))
                                        {
                                            result = regExMessage.Text;
                                            break;
                                        }
                                    }
                                }
                            }
                            // If it's a drop-down list field
                            //else if (associatedField is ComboBoxField && kvp.Value != null)
                            //{
                            //    ComboBoxField comboBoxField = (associatedField as ComboBoxField);
                            //    // Test #1 - make sure the user has chosen a value from the list
                            //    bool foundMatch = false;
                            //    foreach (CodeListValue codeListValue in comboBoxField.ListSource.CodeListValues)
                            //    {
                            //        if (codeListValue.Store == kvp.Value.ToString())
                            //        {
                            //            foundMatch = true;
                            //        }
                            //    }

                            //    if (!foundMatch)
                            //    {
                            //        result = "Only values from the list may be selected.";
                            //    }
                            //}


                            // Run validation rules

                            SuppressErrorNotification = true;

                            foreach (Behavior behavior in this.Form.ValidationBehaviors)
                            {
                                if (behavior.EvaluateCondition(this) == true)
                                {
                                    foreach (IAction action in behavior.Actions)
                                    {
                                        action.Execute(this);

                                        if (action is Action_Invalidate)
                                        {
                                            foreach (string targetName in action.TargetNames)
                                            {
                                                if (targetName.Equals(associatedField.Name, StringComparison.OrdinalIgnoreCase))
                                                {
                                                    Action_Invalidate invalidationAction = (action as Action_Invalidate);
                                                    result = invalidationAction.GetMessage();
                                                }
                                            }
                                        }
                                    }
                                }
                                else
                                {
                                    foreach (IAction action in behavior.ElseActions)
                                    {
                                        action.Execute(this);

                                        if (action is Action_Invalidate)
                                        {
                                            foreach (string targetName in action.TargetNames)
                                            {
                                                if (targetName.Equals(associatedField.Name, StringComparison.OrdinalIgnoreCase))
                                                {
                                                    Action_Invalidate invalidationAction = (action as Action_Invalidate);
                                                    result = invalidationAction.GetMessage();
                                                }
                                            }
                                        }
                                    }
                                }
                            }

                            SuppressErrorNotification = false;
                        }
                    }
                }
                return result;
            }
        }

        #endregion // Properties

        #region Methods

        public override void Validate()
        {
            //base.Validate();
            Errors.Clear();

            List<string> fieldNames = Data.Keys.ToList();
            //foreach (KeyValuePair<string, object> kvp in _data)
            foreach (string fieldName in fieldNames)
            {
                string message = this[fieldName].Trim();
                if (!String.IsNullOrEmpty(message))
                {
                    if (Errors.ContainsKey(fieldName))
                    {
                        List<string> messages = new List<string>();
                        bool success = Errors.TryGetValue(fieldName, out messages);
                        if (success)
                        {
                            messages.Add(message);
                            OnErrorsChanged(fieldName);
                        }
                    }
                    else
                    {
                        List<string> messages = new List<string>() { message };
                        Errors.TryAdd(fieldName, messages);
                    }
                }
            }
        }

        /// <summary>
        /// Initializes the record
        /// </summary>
        /// <param name="form">The form that this record belongs to</param>
        public void Initialize(DataEntryInstrument form)
        {
            Form = form;

            /* This is a very important call; records created during data entry will not have dictionary
             * entries for the fields on the form, and this has been known to cause serious performance
             * degredations in the view. So what we're doing here is making sure that all the fields on 
             * the form are represented in the record. This should be called on a ListChanged or ListUpdated
             * event within the model, ideally.
             */
            foreach (IField field in form.Fields)
            {
                if (field is FixedCellTextBoxField)
                {
                    FixedCellTextBoxField fixedField = field as FixedCellTextBoxField;
                    ObservableCollection<CharacterContainer> characters = new ObservableCollection<CharacterContainer>();
                    for (int i = 0; i < fixedField.Cells; i++)
                    {
                        characters.Add(new CharacterContainer());
                    }
                    AddOrUpdateFieldData(field, characters);
                }
                else if (field is CheckBoxField)
                {
                    CheckBoxField checkBoxField = field as CheckBoxField;

                    // Set the default value for the checkbox, but make sure this doesn't contradict the 'three state' flag
                    if (checkBoxField.IsThreeState == false)
                    {
                        if (checkBoxField.DefaultValue == true)
                        {
                            AddOrUpdateFieldData(field, true);
                        }
                        else
                        {
                            AddOrUpdateFieldData(field, false);
                        }
                    }
                    else
                    {
                        AddOrUpdateFieldData(field, checkBoxField.DefaultValue);
                    }
                }
                else if (field is IInputField)
                {
                    AddOrUpdateFieldData(field, String.Empty);
                }
            }

            HasUnsavedChanges = false;
        }

        /// <summary>
        /// Initializes the record, including the attachment of a foreign key
        /// </summary>
        /// <param name="form">The form that this record belongs to</param>
        /// <param name="foreignKey">The foreign key value to assign to this record</param>
        public void Initialize(DataEntryInstrument form, Guid foreignKey)
        {
            // This overloaded method does the same as 'Initialize(form)' but attaches a foreign key. Make sure to call this one when dealing with grids and related forms.
            Initialize(form);
            this.ForeignKey = foreignKey;
        }

        /// <summary>
        /// Gets the data for a given field
        /// </summary>
        /// <param name="field">The field</param>
        /// <returns>data</returns>
        public object GetFieldData(IField field)
        {
            foreach (KeyValuePair<string, object> kvp in this.Data)
            {
                if (kvp.Key.Equals(field.Name))
                {
                    return kvp.Value;
                }
            }
            return null;
        }

        /// <summary>
        /// Whether or not this record contains a value from a search
        /// </summary>
        /// <param name="searchValue"></param>
        /// <returns>bool; whether or not the value was found</returns>
        public bool ContainsSearchValue(string searchValue)
        {
            // TODO: Look for better case-insensitive searching

            if (searchValue == null || String.IsNullOrEmpty(searchValue.Trim()))
            {
                return true;
            }

            string[] terms = searchValue.Split(' ');

            Dictionary<string, bool> termsFound = new Dictionary<string, bool>();

            foreach (string term in terms)
            {
                termsFound.Add(term, false);
            }

            foreach (KeyValuePair<string, object> kvp in this.Data)
            {
                if((kvp.Value == null || 
                    String.IsNullOrEmpty(kvp.Value.ToString().Trim())) &&
                    String.IsNullOrEmpty(searchValue.Trim()))
                {
                    return true;
                }

                string iFieldValue = String.Empty;
                if (kvp.Value != null)
                {
                    iFieldValue = kvp.Value.ToString();//.ToLower();
                }

                
                foreach(string term in terms)
                {
                    CultureInfo culture = System.Globalization.CultureInfo.InvariantCulture;
                    if (culture.CompareInfo.IndexOf(iFieldValue, term, CompareOptions.IgnoreCase) >= 0)
                    {
                        termsFound[term] = true;
                    }
                }

                
                // old method; used just one item
                //CultureInfo culture = System.Globalization.CultureInfo.InvariantCulture;
                //if(culture.CompareInfo.IndexOf(iFieldValue, searchValue, CompareOptions.IgnoreCase) >= 0)
                ////if (iFieldValue.Contains(searchValue))
                //{
                //    return true;
                //}
            }

            foreach (KeyValuePair<string, bool> kvp in termsFound)
            {
                if (kvp.Value == false)
                {
                    return false;
                }
            }

            return true;
        }

        /// <summary>
        /// Whether or not this record contains a value from a search
        /// </summary>
        /// <param name="searchValue"></param>
        /// <returns></returns>
        //public async Task<bool> ContainsSearchValueAsync(string searchValue)
        //{
        //    var task = await Task.Run(() => ContainsSearchValue(searchValue));

        //    bool success = task;
        //    return success;
        //}

        /// <summary>
        /// Adds or updates data for a given field on this record
        /// </summary>
        /// <param name="field">The field in the record to update</param>
        /// <param name="value">The value of the field</param>
        /// <returns>bool; represents whether the operation was successful</returns>
        public bool AddOrUpdateFieldData(IField field, object value)
        {
            string fieldName = field.Name;
            if (Data.ContainsKey(fieldName)) // we already have this property, so update it with the data
            {
                return UpdateFieldData(field, value);
            }
            else // we don't have already this property, so add it to the internal representation of properties
            {
                return AddFieldData(field, value);
            }
        }

        private bool AddFieldData(IField field, object value)
        {
            string fieldName = field.Name;
            if (Data.ContainsKey(fieldName))
            {
                throw new InvalidOperationException("_data already contains this object");
            }
            else
            {
                FlagFieldAsChanged(fieldName, null);
                HasUnsavedChanges = true;
                Data.Add(fieldName, value);

                return true;
            }
        }

        private void FlagFieldAsChanged(string fieldName, object previousValue)
        {
            if (!DataChanges.ContainsKey(fieldName))
            {
                DataChanges.Add(fieldName, previousValue);
            }
        }

        private bool UpdateFieldData(IField field, object value)
        {
            string fieldName = field.Name;
            if (Data.ContainsKey(fieldName))
            {
                if (Data[fieldName] != null && !Data[fieldName].Equals(value))
                {
                    FlagFieldAsChanged(fieldName, Data[fieldName]);
                    HasUnsavedChanges = true;
                }
                Data[fieldName] = value;
                RaisePropertyChanged(fieldName);
                return true;
            }
            else
            {
                throw new InvalidOperationException("_data does not contain this object");
            }
        }

        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            string fieldName = binder.Name;
            if (Data.Keys.Contains(fieldName))
            {
                object value = Data[fieldName];

                result = value;
                return true;
            }
            else
            {
                result = null;
                return false;
            }
        }

        public override bool TrySetMember(SetMemberBinder binder, object result)
        {
            string fieldName = binder.Name;
            if (Data.Keys.Contains(fieldName))
            {
                if((Data[fieldName] != null && !Data[fieldName].Equals(result)) ||
                    (Data[fieldName] == null && result != null))
                {
                    FlagFieldAsChanged(fieldName, Data[fieldName]);
                    HasUnsavedChanges = true;
                    Data[fieldName] = result;
                    RaisePropertyChanged(binder.Name);
                    RaisePropertyChangedForAssociatedFields(binder.Name);
                }
            }
            else
            {
                FlagFieldAsChanged(fieldName, null);
                HasUnsavedChanges = true;
                Data.Add(fieldName, result);
            }

            return true;
        }

        private void RaisePropertyChangedForAssociatedFields(string fieldName)
        {
            List<string> raised = new List<string>();

            foreach (Behavior behavior in Form.ValidationBehaviors)
            {
                List<IField> fields = behavior.GetFieldsInBehavior(Form);

                foreach (IField field in fields)
                {
                    if (!raised.Contains(field.Name))
                    {
                        RaisePropertyChanged(field.Name);
                        raised.Add(field.Name);
                    }
                }
            }
        }

        [Obsolete("Method should be avoided; use Xml-to-Linq-based method of the same name instead.", true)]
        internal XmlElement Serialize(XmlDocument doc, DataEntryInstrument form)
        {
            XmlElement recordElement = doc.CreateElement("Record");

            XmlAttribute idAttrib = doc.CreateAttribute("Id");
            idAttrib.Value = this.Id.ToString();
            recordElement.Attributes.Append(idAttrib);

            XmlAttribute recAttrib = doc.CreateAttribute("RecStatus");
            recAttrib.Value = this.RecStatus.ToString();
            recordElement.Attributes.Append(recAttrib);

            XmlAttribute fstAttrib = doc.CreateAttribute("FirstSaveTime");
            fstAttrib.Value = this.FirstSaveTime.Ticks.ToString();
            recordElement.Attributes.Append(fstAttrib);

            XmlAttribute lstAttrib = doc.CreateAttribute("LastSaveTime");
            lstAttrib.Value = this.LastSaveTime.Ticks.ToString();
            recordElement.Attributes.Append(lstAttrib);

            XmlAttribute fslAttrib = doc.CreateAttribute("FirstSaveLogon");
            fslAttrib.Value = this.FirstSaveLogOn;
            recordElement.Attributes.Append(fslAttrib);

            XmlAttribute lslAttrib = doc.CreateAttribute("LastSaveLogon");
            lslAttrib.Value = this.LastSaveLogOn;
            recordElement.Attributes.Append(lslAttrib);

            if (this.ForeignKey != null)
            {
                XmlAttribute fkeyAttrib = doc.CreateAttribute("FKey");
                fkeyAttrib.Value = this.ForeignKey.ToString();
                recordElement.Attributes.Append(fkeyAttrib);
            }

            foreach (KeyValuePair<string, object> kvp in Data)
            {
                object rawValue = kvp.Value;
                bool containsField = false;
                if (form.Fields.Contains(kvp.Key))
                {
                    containsField = true;
                }

                if(containsField)
                {
                    XmlElement data = doc.CreateElement("D");

                    XmlAttribute dataName = doc.CreateAttribute("N");
                    dataName.Value = kvp.Key;

                    data.Attributes.Append(dataName);

                    IField field = form.Fields[kvp.Key];

                    if (rawValue != null && rawValue != DBNull.Value)
                    {
                        if (rawValue is DateTime)
                        {
                            data.InnerText = ((DateTime)rawValue).Ticks.ToString();
                        }
                        else if (rawValue is decimal)
                        {
                            data.InnerText = ((decimal)rawValue).ToString(CultureInfo.InvariantCulture);
                        }
                        else if (rawValue is ObservableCollection<CharacterContainer>)
                        {
                            string stringValue = String.Empty;
                            foreach (CharacterContainer c in rawValue as ObservableCollection<CharacterContainer>) 
                            {
                                stringValue += c.Character;
                            }
                            data.InnerText = stringValue;
                        }
                        else if (rawValue is byte[])
                        {
                            data.InnerText = rawValue.ToString();
                        }
                        else
                        {
                            if (field is ComboBoxField)
                            {
                                data.InnerText = rawValue.ToString();
                            }
                            else
                            {
                                data.InnerText = rawValue.ToString();
                            }
                        }
                    }
                    else if (form.Fields[kvp.Key] is FixedCellTextBoxField)
                    {
                        FixedCellTextBoxField fixedTextBoxField = form.Fields[kvp.Key] as FixedCellTextBoxField;
                        string stringValue = String.Empty;
                        for (int i = 0; i < fixedTextBoxField.Cells; i++)
                        {
                            stringValue += ' ';
                        }
                        data.InnerText = stringValue;
                    }
                    else
                    {
                        data.InnerText = String.Empty;
                    }

                    if (!String.IsNullOrEmpty(data.InnerText))
                    {
                        recordElement.AppendChild(data);
                    }
                }
            }

            // Add stuff that isn't included in _data, e.g. new fields added since data were last entered.
            // All should be blank.
            //foreach (IField field in form.Fields)
            //{
            //    if (!_data.ContainsKey(field.Name) && field is IInputField)
            //    {
            //        XmlElement data = doc.CreateElement("Data");

            //        XmlAttribute dataName = doc.CreateAttribute("Name");
            //        dataName.Value = field.Name;

            //        data.Attributes.Append(dataName);

            //        recordElement.AppendChild(data);
            //    }
            //}

            return recordElement;
        }

        internal XElement Serialize(XDocument doc, DataEntryInstrument form)
        {
            XElement recordElement = 
                new XElement("Record",
                    new XAttribute("Id", this.Id.ToString()),
                    new XAttribute("RecStatus", this.RecStatus.ToString()),
                    new XAttribute("FirstSaveTime", this.FirstSaveTime.Ticks.ToString()),
                    new XAttribute("LastSaveTime", this.LastSaveTime.Ticks.ToString()),
                    new XAttribute("FirstSaveLogon", this.FirstSaveLogOn),
                    new XAttribute("LastSaveLogon", this.LastSaveLogOn),
                    new XAttribute("FKey", this.ForeignKey.ToString()));

            foreach (KeyValuePair<string, object> kvp in Data)
            {
                object rawValue = kvp.Value;
                bool containsField = false;
                if (form.Fields.Contains(kvp.Key))
                {
                    containsField = true;
                }

                if (containsField)
                {
                    XElement data =
                        new XElement("D",
                            new XAttribute("N", kvp.Key));

                    IField field = form.Fields[kvp.Key];

                    if (rawValue != null && rawValue != DBNull.Value)
                    {
                        if (rawValue is DateTime)
                        {
                            data.Value = ((DateTime)rawValue).Ticks.ToString();
                        }
                        else if (rawValue is decimal)
                        {
                            data.Value = ((decimal)rawValue).ToString(CultureInfo.InvariantCulture);
                        }
                        else if (rawValue is ObservableCollection<CharacterContainer>)
                        {
                            string stringValue = String.Empty;
                            foreach (CharacterContainer c in rawValue as ObservableCollection<CharacterContainer>)
                            {
                                stringValue += c.Character;
                            }
                            data.Value = stringValue;
                        }
                        else if (rawValue is byte[])
                        {
                            data.Value = Convert.ToBase64String((byte[])rawValue);
                        }
                        else
                        {
                            if (field is DateBoxField)
                            {
                                DateTime testValue = DateTime.Now;
                                bool success = DateTime.TryParse(rawValue.ToString(), System.Globalization.CultureInfo.InvariantCulture, DateTimeStyles.None, out testValue);
                                if (success)
                                {
                                    data.Value = testValue.Ticks.ToString();
                                }
                            }
                            else if (field is ComboBoxField)
                            {
                                data.Value = rawValue.ToString();
                            }
                            else
                            {
                                data.Value = rawValue.ToString();
                            }
                        }
                    }
                    else if (form.Fields[kvp.Key] is FixedCellTextBoxField)
                    {
                        FixedCellTextBoxField fixedTextBoxField = form.Fields[kvp.Key] as FixedCellTextBoxField;
                        string stringValue = String.Empty;
                        for (int i = 0; i < fixedTextBoxField.Cells; i++)
                        {
                            stringValue += ' ';
                        }
                        data.Value = stringValue;
                    }
                    else
                    {
                        data.Value = String.Empty;
                    }

                    if (!String.IsNullOrEmpty(data.Value))
                    {
                        recordElement.Add(data);
                    }
                }
            }

            return recordElement;
        }

        
        #endregion // Methods
    }
}
